package tr.edu.ozyegin.ccrg.raid.network;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import tr.edu.ozyegin.ccrg.raid.master.Master;

public class Client extends Network{
	private String IPAddress = "";
	private Socket socket;
	private DataInputStream stIn = null;
	private DataOutputStream stOut = null;
	private ClientType type = null;
	public Client(String IPAddress,int port,ClientType type){
		super(port);
		this.IPAddress = IPAddress;
		this.type = type;
	}
	public Client(Socket socket){
		super(0);
		this.IPAddress = "localhost";
		this.port = 0;
		this.socket = socket;
		this.isConnected = true;
	}
	public void setClientType(ClientType type){
		this.type = type;
	}
	public ClientType getClientType(){
		return this.type;
	}
	public void NoDelay(boolean delay) throws SocketException{
		this.socket.setTcpNoDelay(delay);
	}
	public void Connect() throws IOException, InterruptedException{
		if(!this.isConnected && this.type!=null){
			try {
				for(int i = 0; i < 10; i++){
					try{
						System.out.println("trying to connect attempt "+i);
						this.socket = new Socket(this.IPAddress,this.port);
						break;
					}
					catch (UnknownHostException e) {
						Thread.currentThread().sleep(100);
					}
				}
				
				this.Write(this.type.toString());
				this.isConnected = true;
				boolean flag = false;
				while(true){
					Short opCode = (Short)this.Read(Short.class);
					switch(opCode){
					case 0:
						System.out.println("correct control massege received");
						break;
					default:
						System.out.println("control sequence broken");
						flag = true;
					}
					if(flag)break;
				}
			} catch (IOException e) {
				throw e;
			}
		}
		else System.out.println("Please set the Client Type");
	}
	public void Disconnect(){
		try {
			this.socket.close();
		} catch (IOException e) {
		}
	}
	public boolean isConnected(){
		System.out.println("isclosed" + this.socket.isClosed());
		System.out.println(this.socket.isOutputShutdown() + " " + this.socket);
		return (this.socket != null)?this.socket.isConnected() : false;
	}
	protected void finalize (){
		if(this.socket.isConnected()){
			try {
				this.socket.close();
			} catch (IOException e) {
				System.out.println("Cannot close socket...");
			}
		}
	}
	public int getPort(){
		return this.socket.getLocalPort();
	}
	public int WriteBytes(byte[] toWrite, boolean writeLength) throws IOException{
		if(this.socket.isConnected()){
			if(writeLength) this.Write(new Integer(toWrite.length));
			//System.out.println("sal1 "+ toWrite.length);
			
			this.socket.getOutputStream().write(toWrite);
			//System.out.println("sal2 ");
			return (writeLength) ? toWrite.length + 4 : toWrite.length;
		}
		return -1;
	}
	public int Write(Object o) throws IOException{
		if(this.socket.isConnected()){
			if(o instanceof Integer){
				//ByteBuffer bb = ByteBuffer.allocate(4);
				Integer i = (Integer)o;
				System.out.println("integer written "+i);
				return this.WriteBytes(intToByteArray(i),false);
			}
			else if(o instanceof Long){
				//ByteBuffer bb = ByteBuffer.allocate(4);
				Long i = (Long)o;
				System.out.println("long written "+i);
				return this.WriteBytes(longToByteArray(i),false);
			}
			else if(o instanceof Short){
				//ByteBuffer bb = ByteBuffer.allocate(2);
				Short i = (Short)o;
				
				System.out.println("short written "+i);
				//ByteBuffer bs = bb.duplicate();
				//System.out.println("short written " + bs.getShort());
				//System.out.println("array size "+ bb.array().length);
				return this.WriteBytes(shortToByteArray(i),false);
			}
			else{
				String i = (String)o;
				System.out.println("string written "+i);
				return this.WriteBytes(i.getBytes(),true);
			}
		}
		return -1;
	}
	public static final byte[] intToByteArray(int value) {
        return new byte[] {
                (byte)(value >>> 24),
                (byte)(value >>> 16),
                (byte)(value >>> 8),
                (byte)value};
	}
	public static final int byteArrayToInt(byte [] b) {
        return (b[0] << 24)
                + ((b[1] & 0xFF) << 16)
                + ((b[2] & 0xFF) << 8)
                + (b[3] & 0xFF);
	}
	public static final byte[] longToByteArray(long value) {
        return new byte[] {
        		(byte)(value >>> 56),
        		(byte)(value >>> 48),
        		(byte)(value >>> 40),
        		(byte)(value >>> 32),
                (byte)(value >>> 24),
                (byte)(value >>> 16),
                (byte)(value >>> 8),
                (byte)value};
	}
	public static final long byteArrayToLong(byte [] b) {
        return (b[0] << 56)
        		+ ((b[1] & 0xFF) << 48)
        		+ ((b[2] & 0xFF) << 40)
        		+ ((b[3] & 0xFF) << 32)
        		+ ((b[4] & 0xFF) << 24)
                + ((b[5] & 0xFF) << 16)
                + ((b[6] & 0xFF) << 8)
                + (b[7] & 0xFF);
	}
	public static final byte[] shortToByteArray(short value) {
        return new byte[] {(byte)(value >>> 8),(byte)value};
	}
	public static final short byteArrayToShort(byte [] b) {
        return  (short) (((b[0] & 0xFF) << 8) + (b[1] & 0xFF));
	}
	public static byte[] changeByteOrder(byte[] src){
		if(ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN)return src;
		byte[] result = new byte[src.length];
		for(int i =0, j = result.length-1 ; i < result.length ; i++ , j--){
			result[i]=src[j];
		}
		return result;
	}
	public int ReadBytes(byte[] buffer) throws IOException{
		if(this.socket.isConnected()){
			int length = buffer.length;
			int result  = 0;
			int max = (Short.MAX_VALUE * 2) + 2;
			if(length > max){
				int iteration = (int)Master.Ceil(length, max);
				int toRead = max;
				for(int i = 0 ; length - result != 0 ; i++){
					if(i == iteration - 1) toRead = (length - (i * max));
					toRead = length - result;
					
					result += this.socket.getInputStream().read(buffer, result, toRead);
					//System.out.println("offset " + result + " length "+ length + " toread "+toRead);
				}
			}
			else{
				result = this.socket.getInputStream().read(buffer);
			}
			return result;
		}
		return -1;
	}
	public Object Read(Class<?> type) throws IOException{
		if(this.socket.isConnected()) {
			if(type.equals(Integer.class)){
				byte[] tmp = new byte[4];
				this.ReadBytes(tmp);
				//ByteBuffer bb = ByteBuffer.wrap(tmp);
				//bb.order(ByteOrder.nativeOrder());
				//Integer i = new Integer(bb.getInt());
				Integer i = new Integer(byteArrayToInt(tmp));
				System.out.println("readed as int " + i); 
				//tmp = changeByteOrder(tmp);
				//Integer b = new Integer(byteArrayToInt(tmp));
				//System.out.println("readed as int " + b); 
				return i;
			}
			else if(type.equals(Long.class)){
				byte[] tmp = new byte[8];
				this.ReadBytes(tmp);
				Long i = new Long(byteArrayToLong(tmp));
				System.out.println("readed as long " + i);
				return i;
			}
			else if(type.equals(Short.class)){
				byte[] tmp = new byte[2];
				this.ReadBytes(tmp);
				//ByteBuffer bb = ByteBuffer.wrap(tmp);
				//bb.order(ByteOrder.nativeOrder());
				Short i = new Short(byteArrayToShort(tmp));
				System.out.println("readed as short " + i);
				return i;
			}
			else{
				Integer count = (Integer)this.Read(Integer.class);
				byte[] tmp = new byte[count];
				this.ReadBytes(tmp);
				String i = new String(tmp);
				System.out.println("readed as string " + i);
				return i;
			}
		}
		return null;
	}
}
